﻿using EditoneLib;
using EditoneLib.Annotations;
using EditoneLibRT.Exceptions;
using EditoneLibRT.WaveStreams;
using NAudio.Wave;
using NAudio.Wave.SampleProviders;
using System;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using Windows.Foundation.Metadata;
using Windows.Storage;
using Windows.Storage.Streams;
using EditoneLib.Streams.Audio;
using NAudio.CoreAudioApi;
using NAudio.Win8.Wave.WaveOutputs;
using Buffer = System.Buffer;

namespace EditoneLibRT
{
    [Activatable(1)]
    public sealed class Clip
    {
        internal const string LoadingType = "_Loading";
        private const int ResampleBufferSize = 1000;
        internal readonly Task<Track> Track;
        internal dynamic DyClip;
        internal IClip StClip;
        private Track _track;
        public bool IsLoading {
            get { return Type == LoadingType; }
        }

        internal Clip(IClip clip, Track track = null)
        {
            Track = new Task<Track>(() => TrackValue);
            TrackValue = track;
            SetIClip(clip);
        }

        private Clip(Track track = null)
        {
            Track = new Task<Track>(() => TrackValue);
            TrackValue = track;
        }

        public event EventHandler<string> PropertyChanged;

        public long Duration
        {
            get
            {
                if (IsLoading)
                {
                    return LoadingStream.TotalTime.Ticks;
                }
                return StClip.TotalLength.Ticks;
            }
        }

        public bool IsAudioClip { get; private set; }

        public Windows.Storage.Streams.IInputStream Stream
        {
            get
            {
                if (IsAudioClip)
                {
                    return ((Stream) DyClip.Stream).AsInputStream();
                }
                else
                {
                    throw new InvalidOperationException("This is not an audio clip.");
                }
            }
            set
            {
                if (IsAudioClip)
                {
                    if (Equals(value, DyClip.Stream)) return;
                    DyClip.Stream = value.AsStreamForRead();
                    OnPropertyChanged();
                }
                else
                {
                    throw new InvalidOperationException("This is not an audio clip.");
                }
            }
        }

        public Track TrackValue
        {
            get { return _track; }
            set
            {
                if (value != _track)
                {
                    _track = value;
                    Track.Start();
                }
            }
        }

        public string Type { get; private set; }
        internal MediaFoundationReader LoadingStream { get; set; }

        public Clip[] Split(long atLong)
        {
            var at = TimeSpan.FromTicks(atLong);
            var track = _track.DyTrack as IAudioTrack;
            if (track == null || IsLoading)
            {
                throw new InvalidOperationException("This clip's track is not an audio track.");
            }

            var clips = _track.DyTrack.SplitClip(at);
            return _track.Clips.Where((kvp) => clips.Item1 == kvp.Value.StClip || clips.Item2 == kvp.Value.StClip)
                .OrderBy((kvp) => kvp.Key).Select((kvp) => kvp.Value).ToArray<Clip>();
        }

        /// <summary>
        /// Detects if a clip can move to a position. If not, attempts to suggest a suitable position.
        /// </summary>
        /// <param name="track">Target track</param>
        /// <param name="offsetTicks">Desired target offset</param>
        /// <returns>Struct containing data as to whether the clip can move and, if not, the suggested position.</returns>
        public CanMoveStruct CanMoveTo(Track track, long offsetTicks)
        {
            var result = new CanMoveStruct() {
                CanMove = false
            };

            // When loading clips, we don't know their exact length. 
            // Therefore we can't accurately tell if a clip fits in a gap or not.
            if (IsLoading) {
                return result;
            }

            // Clips can't be dragged into tracks that don't support them
            var clipTypeInfo = StClip.GetType().GetTypeInfo();
            var clipIsSupported = track.StTrack.SupportedClipTypes.Aggregate(
                false,
                (or, type) => or || type.GetTypeInfo().IsAssignableFrom(clipTypeInfo)
                );

            if (!clipIsSupported)
            {
                return result;
            }

            var offset = TimeSpan.FromTicks(offsetTicks);
            var occupiedStart = offset;
            var occupiedEnd = offset + StClip.TotalLength;

            result.CanMove = true;
            var prevCount = 0;

            // Check for clips in range [occupiedStart, occupiedEnd)
            // If some found, expand range to include them
            while (true) {
                var intersectClips = track.StTrack.Where((kvp) => {
                    return kvp.Key < occupiedEnd && kvp.Key + kvp.Value.TotalLength >= occupiedStart && kvp.Value != StClip;
                });

                var nowCount = intersectClips.Count();

                if (nowCount <= prevCount)
                {
                    break;
                } 
                else 
                {
                    result.CanMove = false;
                }

                prevCount = nowCount;

                var headObstacle = intersectClips.Min((kvp) => kvp.Key);
                var tailObstacle = intersectClips.Max((kvp) => kvp.Key + kvp.Value.TotalLength);

                var newStart = occupiedStart = headObstacle - StClip.TotalLength;
                var newEnd = occupiedEnd = tailObstacle + StClip.TotalLength;
                
                if ((newStart + newEnd).Ticks / 2 > offset.Ticks + StClip.TotalLength.Ticks / 2) 
                {
                    if (newStart < TimeSpan.Zero)
                    {
                        result.SuggestedOffset = null;
                    }
                    else
                    {
                        result.SuggestedOffset = newStart.Ticks;
                    }
                } 
                else 
                {
                    result.SuggestedOffset = tailObstacle.Ticks;
                }
            }

            return result;
        }

        public void Move(Track track, long offset)
        {
            this.TrackValue.RemoveClip(this);
            track.AddClip(TimeSpan.FromTicks(offset), this.StClip);
        }

        public void FadeIn()
        {
            if (!IsAudioClip) return;
            var clip = (BaseAudioClip) StClip;
            clip.Stream = new FadeStream(clip.Stream, FadeStream.FadeDirection.In);
        }

        public void FadeOut()
        {
            if (!IsAudioClip) return;
            var clip = (BaseAudioClip)StClip;
            clip.Stream = new FadeStream(clip.Stream, FadeStream.FadeDirection.Out);
        }

        public void Normalize()
        {
            if (!IsAudioClip) return;
            var clip = (BaseAudioClip)StClip;
            clip.Stream = new NormalizeStream(clip.Stream);
        }

        public void Delete()
        {
            TrackValue.RemoveClip(this);
        }

        internal static Clip Load(StorageFile file)
        {
            var clip = new Clip()
            {
                IsAudioClip = false,
                Type = LoadingType,
                LoadingStream = new MediaFoundationReader(file) 
            };

            

            if (clip.LoadingStream.WaveFormat.Channels > 2)
            {
                ChannelCountLimitException.Throw();
            }

            return clip;
        }

        internal async Task ConvertAsync()
        {
            LoadingStream.Position = 0;
            var converter = new WdlResamplingSampleProvider(LoadingStream.ToSampleProvider(), Settings.SampleRate);

            if (LoadingStream.Length > int.MaxValue)
            {
                StreamSizeLimitException.Throw();
            }
            var stream = new MemoryStream();

            await Task.Run(() =>
            {
                var floatBuffer = new float[ResampleBufferSize];
                var byteBuffer = new byte[ResampleBufferSize * sizeof(float)];
                while (true)
                {
                    int length = converter.Read(floatBuffer, 0, floatBuffer.Length);

                    if (length <= 0)
                    {
                        break;
                    }

                    if (stream.Length + length > int.MaxValue)
                    {
                        StreamSizeLimitException.Throw();
                    }

                    Buffer.BlockCopy(floatBuffer, 0, byteBuffer, 0, length * sizeof(float));
                    stream.Write(byteBuffer, 0, length * sizeof(float));
                }
            });
            stream.Position = 0;

            IClip clip = null;
            switch (LoadingStream.WaveFormat.Channels)
            {
                case 1:
                    clip = new MonoAudioClip(stream);
                    break;

                case 2:
                    clip = new StereoAudioClip(stream);
                    break;

                default:
                    ChannelCountLimitException.Throw();
                    break;
            }

            var track = Track.Result;
            track.RemoveClip(this);
            track.StTrack.Add(
                TimeSpan.FromTicks(
                    track.Clips.FirstOrDefault(kvp => kvp.Value == this).Key
                    ),
                clip
                );
        }

        private void DetermineType()
        {
            Type audioClipType = typeof (EditoneLib.BaseAudioClip);

            System.Type type = StClip.GetType();

            Type = type.Name;

            while (type != null)
            {
                var typeInfo = type.GetTypeInfo();
                if (typeInfo.AsType() == audioClipType)
                {
                    IsAudioClip = true;
                    return;
                }
                type = typeInfo.BaseType;
            }
            IsAudioClip = false;
        }

        [NotifyPropertyChangedInvocator]
        private void OnPropertyChanged([CallerMemberName] string propertyName = null)
        {
            var handler = PropertyChanged;
            if (handler != null) handler(this, propertyName);
        }

        private void SetIClip(IClip clip)
        {
            StClip = clip;
            DyClip = clip;
            StClip.PropertyChanged += StClipOnPropertyChanged;
            DetermineType();
        }

        
        private void StClipOnPropertyChanged(object sender, PropertyChangedEventArgs propertyChangedEventArgs)
        {
            OnPropertyChanged(propertyChangedEventArgs.PropertyName);
        }
    }

    public struct CanMoveStruct {
        public bool CanMove;
        public long? SuggestedOffset;
    }
}